getOutputStream() has already been called for this response的来路

除了上篇的response has been committed,另一个常见的异常getOutputStream() has alreadybeen called for this response 或 getWriter() has already been called for this response。这类异常并非Tomcat专有的,在weblogic中是为strict servlet API: cannot call getWriter() after getOutputStream 或 strict servlet API: cannot call getOutputStream() after getWriter().

意思就是调用了getWriter之后,不能再调用getOutputStream, 反之亦然。getWriter和getOutputStream都是servlet引擎用来向响应报文中写入数据的方法。那为什么要限制同时使用呢?

getWriter()方法返回是PrintWriter对象,PrintWriter用来写入字符或字符串。getOutputStream()返回ServletOutputStream,其接口为OutputStream,所以getOutputStream()方法用来写入字节流。

getWriter和getOutputStream就像向响应报文中输入数据的管子,而且两个管子中的数据格式不一样。如果两个管子同时向响应报文中灌入数据,那数据是不是就乱了呢? 所以中间件安排他俩互斥使用。

既然PrintWriter用来写入字符或字符串,那把PrintWriter写入的字符串转换成字节流不就可以同时用了。其实PrintWriter写入数据的时候有可能响应字符集还没有确定,你无法知道应该用哪个字符集去转换。(Tomcat提供了ENFORCE_ENCODING_IN_GET_WRITER参数)。具体其他的原因,只能咨询规范的作者了。在Servlet2.5规范SRV.15.2.22节提到:Either this method or getWriter() may becalled to write the body, not both.

什么情况下出现前面提到的异常,一般在我们需要输出一种特殊格式的响应内容时会出现。比如验证码,导出文件等。大多程序员会把这些业务放在Jsp中,然后通过response.getOutputStream().write()输出数据。而Jsp页面中的可能含有html内容,即使只是一个空格或者换行。这些内容是通过out.write输出的, 这个out其实就response.getWriter()的值。这样就会在你不经意中被调用response.getWriter(),可能位于response.getOutputStream()调用之前,也可能在之后。无论如何,异常是跑不了啦。

解决这类问题的方法当前很简单,就是隐藏的response.getWriter()的调用:

  1. 使用Servlet输出验证码,导出文件。
  2. 如果确要用JSP文件,最好的办法看编译后的java文件,有没有out.write操作。有的话,检查是JSP哪一行引起的,删除即可。
    大多数情况出现这个异常,并没影响正常业务。其实异常是后一个get调用引起的,而且这个调用非常大的机率是不需要的,调用他的时候响应报文大概已经在去向客户端的路上了。